上一篇文章 把 TENT 的请求路径压缩成了一串箭头。那串箭头没有错,但它隐藏了代码走读时最容易断掉的几处连接:公共 Request 在哪里变成 TaskInfo,为什么 selector 会返回 GDS,一个逻辑 task 怎样展开成多个 cuFile slice,以及完成事件怎样重新聚合成公共状态。
本文固定在 Mooncake 提交 89da2c3a ,只追踪一次成功的 GDS 读取 。每个关键节点都给出实际会执行的 C++ 片段;代码语句保持原样,只增加中文走读注释和明确的省略标记。
先给出结论:一个 TENT 文件读取不会从 submitTransfer 直接跳到 cuFileBatchIOSubmit。中间至少发生五次对象换手:
1 2 3 4 5 6 7 Request → PreparedSubmit::Owner → Batch::TaskInfo → GdsSubBatch::IOParamRange → CUfileIOParams_t[1..N] → CUfileIOEvents_t[1..N] → TransferStatus
先限定这条路径 “正常路径”必须有明确边界,否则代码里的可选机制会被误写成必经步骤。本文采用以下条件:
Mooncake 构建时定义了 USE_GDS,配置中显式设置 transports/gds/enable=true。
不自定义 transport policy,文件 Segment 使用默认候选顺序 GDS → IOURING。
使用默认 enable_runtime_queue=false,因此 prepareSubmit 后直接进入 commitPreparedSubmit。
只提交一个 Request::READ。默认的 merge_requests=true 仍会执行,但单请求不会发生合并。
file:// 指向普通文件,GDS 文件 handle、batch handle 和 cuFile I/O 均成功。
调用者主动轮询状态,并在看到 COMPLETED 后释放 Batch。
这些默认值和 GDS 开关分别落在下面两处。这里要注意一个看起来有些反直觉的组合:runtime queue 默认关闭,request merge 默认开启,而 GDS 即使编译进来,运行时也默认关闭。
1 2 3 4 5 6 7 merge_requests_ = conf_->get ("merge_requests" , true ); max_failover_attempts_ = conf_->get ("max_failover_attempts" , 3 ); enable_auto_failover_on_poll_ = conf_->get ("enable_auto_failover_on_poll" , true ); enable_progress_worker_ = conf_->get ("enable_progress_worker" , false ); runtime_queue_config_.enabled = conf_->get ("enable_runtime_queue" , false );
源码:transfer_engine_impl.cpp:348-354 。
1 2 3 4 5 #ifdef USE_GDS if (conf_->get ("transports/gds/enable" , false )) transport_list_[GDS] = std::make_shared <GdsTransport>(); #endif
源码:transport_loader.cpp:87-90 。
下图只画上述成功路径。蓝色实线是实际函数调用,灰色虚线是状态返回;图里没有画 queue、failover、staging 和 cancellation。
图:基于 Mooncake 89da2c3a 源码整理的自绘时序图。公共 task 进入 GDS 后可能展开为多个 slice,轮询结果再沿相反方向聚合为 TransferStatus。
请求进入 TENT 前有什么 Request 描述的是谁搬到哪里公共请求本身很薄:操作方向、本地指针、目标 Segment、目标偏移和长度是执行路径真正需要的字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Request { enum OpCode { READ, WRITE }; OpCode opcode; void * source; SegmentID target_id; uint64_t target_offset; size_t length; int priority = PRIO_HIGH; std::optional<std::string> policy_name; TransportType transport_hint = UNSPEC; uint64_t deadline_ns = 0 ; IntentType intent_type = IntentType::INTENT_UNSPEC; };
源码:types.h:132-153 。
source 这个名字在 READ 路径上容易误导:GDS 最终把它填入 CUfileIOParams_t::devPtr_base,所以读文件时它实际是本地接收 buffer ;写文件时才是本地数据源。target_offset 始终是文件偏移。
Segment handle 还不是文件 handle 调用者先用 file://path 得到 SegmentID。openSegment 在这一刻只登记名字与 ID,并没有执行 POSIX open:
1 2 3 4 5 6 7 8 9 Status TransferEngineImpl::openSegment (SegmentID& handle, const std::string& segment_name) { if (segment_name.empty () || segment_name == local_segment_name_) { handle = LOCAL_SEGMENT_ID; return Status::OK (); } return metadata_->segmentManager ().openRemote (handle, segment_name); }
源码:transfer_engine_impl.cpp:640-647 ,ID 映射见 segment_manager.cpp:47-60 。
第一次解析这个 Segment 时,getRemote 才根据 file:// 构造 FileSegmentDesc 并用 stat 确认文件存在;真正的 open(O_DIRECT) 和 cuFileHandleRegister 还要等到 GDS 提交阶段。对应代码分别在 segment_manager.cpp:142-158 、segment_manager.cpp:192-217 和 gds_transport.cpp:32-68 。
Batch 只是公共 task 容器 allocateBatch(1) 分配的是 TENT 公共 Batch。此时还没有 GDS SubBatch,也没有 cuFile batch handle:
1 2 3 4 5 6 7 8 9 BatchID TransferEngineImpl::allocateBatch (size_t batch_size) { Batch* batch = Slab<Batch>::Get ().allocate (); if (!batch) return (BatchID)0 ; batch->max_size = batch_size; batch->task_list.reserve (batch_size); BatchID batch_id = (BatchID)batch; return batch_id; }
源码:transfer_engine_impl.cpp:898-912 。
节点一:公共 API 进入实现层 TransferEngine 的公开函数没有调度逻辑,它把 Batch、请求列表和状态对象直接交给 TransferEngineImpl:
1 2 3 4 5 6 7 8 9 10 Status TransferEngine::submitTransfer ( BatchID batch_id, const std::vector<Request>& request_list) { return impl_->submitTransfer (batch_id, request_list); } Status TransferEngine::getTransferStatus (BatchID batch_id, size_t task_id, TransferStatus& task_status) { return impl_->getTransferStatus (batch_id, task_id, task_status); }
源码:transfer_engine.cpp:142-145 、transfer_engine.cpp:171-174 。
实现层先 retain Batch,再准备请求。由于本文限定 runtime queue 关闭,shouldQueueSubmit 返回 false,所以当前线程会直接执行 commitPreparedSubmit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Status TransferEngineImpl::submitTransfer ( BatchID batch_id, const std::vector<Request>& request_list, const Notification* notifi, QueueOwnerKind owner_kind) { Batch* batch = nullptr ; CHECK_STATUS (retainBatch (batch_id, batch)); BatchRef batch_ref (*this , batch) ; const size_t start_task_id = batch_ref.get ()->task_list.size (); PreparedSubmit prepared; CHECK_STATUS (prepareSubmit (batch_ref.get (), request_list, prepared)); if (shouldQueueSubmit (prepared, owner_kind)) { } else { CHECK_STATUS (commitPreparedSubmit (batch_ref.get (), prepared)); } return batch_ref.release (); }
源码:transfer_engine_impl.cpp:2160-2187 。
这里的返回值只表示准备与底层提交是否成功 ,不表示文件读取已经完成。NVIDIA 对 cuFile Batch API 的定义是:提交调用本身同步返回,但 I/O 相对 host thread 异步执行,完成要通过 status API 查询。cuFile Batch API
节点二:准备请求并选择 GDS Request 先变成 PreparedSubmit::OwnerprepareSubmit 做三件事:校验 hint、尝试合并连续请求、为每个合并后的 owner 解析 transport。对本文的单请求,merged.request_list 仍只有一个元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Status TransferEngineImpl::prepareSubmit ( Batch* batch, const std::vector<Request>& request_list, PreparedSubmit& prepared) { prepared = PreparedSubmit{}; const size_t start_task_id = batch->task_list.size (); prepared.submit_time = std::chrono::steady_clock::now (); auto merge_boundaries = merge_requests_ ? resolveRequestBoundaries (metadata_.get (), request_list) : std::vector<RequestBoundaryInfo>{}; auto merged = mergeRequests (request_list, merge_boundaries, merge_requests_); prepared.owners.reserve (merged.request_list.size ()); for (const auto & request : merged.request_list) { PreparedSubmit::Owner owner; owner.request = request; owner.route = resolveTransport (owner.request, 0 ); prepared.owners.push_back (std::move (owner)); } return Status::OK (); }
源码:transfer_engine_impl.cpp:1650-1696 。
这一步结束后还没有 TaskInfo,只有一份临时计划:
Owner.request 保存可能经过合并的物理请求;
Owner.route 保存 selector 的结果;
PreparedSubmit::Task 保存公共 task ID 与 owner 的映射。
selector 为什么返回 GDS getTransportType 先读取 target_id 对应的 Segment 描述,再把本地指针识别成 CPU 或 CUDA memory。对 File Segment,它不会查远端 buffer 的 transport 列表,而是构造文件选择上下文:
1 2 3 4 5 6 7 8 9 10 11 12 13 if (desc->type == SegmentType::File) { ctx.segment_type = SegmentType::File; ctx.same_machine = true ; ctx.local_memory_type = local_mtype; ctx.remote_memory_type = MTYPE_CPU; ctx.buffer_transports = nullptr ; } else { } return transport_selector_->select (ctx, transport_list_, transport_index, hint);
源码:transfer_engine_impl.cpp:1217-1313 。
默认 file_storage policy 给出的原始候选是 {GDS, IOURING}。selector 按顺序跳过不存在或 capability 不匹配的 transport;GDS 安装时声明了 dram_to_file=true 和 gpu_to_file=true,因此满足本文条件时,第一个候选就是 GDS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const auto & raw = !matching_policy->transports.empty () ? matching_policy->transports : context.buffer_transports ? *context.buffer_transports : kEmpty; auto candidates = reorderWithHint (raw, hint);if (!candidates) return result;for (size_t i = 0 ; i < candidates->size (); ++i) { TransportType type = (*candidates)[i]; if (!isTransportAvailable (type, context, available_transports)) continue ; if (transport_index == 0 ) { result.transport = type; break ; } --transport_index; }
源码:默认 policy 见 transport_selector.cpp:84-102 ,capability 检查见 transport_selector.cpp:456-515 ,候选选择见 transport_selector.cpp:518-593 。
selector 只证明 TENT 选择了 GdsTransport。实际 I/O 是否完全避开 bounce buffer 还取决于 buffer 注册、对齐、文件系统、拓扑和 cuFile 兼容模式;TENT 这条路径没有读取 cuFile stats 来证明物理数据路径。
节点三:公共 task 进入 GDS SubBatch commitPreparedSubmit 才把临时计划写进长期存在的 Batch::task_list。正常单请求会生成一个 TaskInfo,状态初始化为 PENDING,type 设为 GDS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 for (const auto & task_plan : prepared.tasks) { size_t task_id = task_plan.task_id; size_t merged_task_id = task_plan.merged_task_index; auto & task = batch->task_list[task_id]; const auto & owner = prepared.owners[merged_task_id]; auto & merged_request = owner.request; task.failover_count = 0 ; task.xport_priority = 0 ; task.status = PENDING; task.request = merged_request; task.staging = false ; task.start_time = prepared.submit_time; task.dispatch_time = prepared.submit_time; task.type = owner.route.transport; task.device_mask = owner.route.device_mask; if (!batch->sub_batch[task.type]) { auto & transport = transport_list_[task.type]; auto status = transport->allocateSubBatch ( batch->sub_batch[task.type], batch->max_size); attachProgressNotifier (batch, batch->sub_batch[task.type]); } task.sub_task_id = -1 ; task.derived = false ; physical_task_id_list[task.type].push_back (task_id); }
源码:transfer_engine_impl.cpp:1727-1802 。
allocateSubBatch 创建的是 GDS 私有容器。它从池中取 BatchHandle;池为空时调用 cuFileBatchIOSetUp,然后准备参数、事件和稳定状态缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (!batch_handle || batch_handle->max_nr != io_batch_depth_) { batch_handle = new BatchHandle (); batch_handle->max_nr = io_batch_depth_; auto result = cuFileBatchIOSetUp (&batch_handle->handle, io_batch_depth_); } gds_batch->batch_handle = batch_handle; gds_batch->max_size = max_size; gds_batch->io_events.resize (io_batch_depth_); gds_batch->io_params.clear (); gds_batch->io_params.reserve (io_batch_depth_); gds_batch->io_param_ranges.clear (); gds_batch->cached_events.clear (); gds_batch->cached_events.reserve (io_batch_depth_); gds_batch->reusable = true ; gds_batch->cancel_requested = false ;
源码:gds_transport.cpp:303-361 。
TENT 随后用 sub_batch->size() 计算 sub_task_id,收集这个 transport 的请求,并真正调用后端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int next_sub_task_id = static_cast <int >(sub_batch->size ());for (const auto physical_task_id : group) { for (const auto public_task_id : public_tasks_by_physical_owner.at (physical_task_id)) { batch->task_list[public_task_id].sub_task_id = next_sub_task_id; } ++next_sub_task_id; } std::vector<Request> requests; requests.reserve (group.size ()); for (const auto task_id : group) requests.push_back (batch->task_list[task_id].request); auto status = transport->submitTransferTasks (sub_batch, requests);
源码:transfer_engine_impl.cpp:1829-1877 。
到这里,公共 task 与 GDS task 已经有了稳定映射:
1 2 3 Batch::task_list[public_task_id] .type = GDS .sub_task_id = GdsSubBatch::io_param_ranges 的下标
节点四:GDS 把一个 task 展开为 slice GdsTransport::submitTransferTasks 做的不是简单参数转发。它先延迟打开文件,再把每个逻辑请求切成不超过 16 MiB 的物理 slice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 Status GdsTransport::submitTransferTasks ( SubBatchRef batch, const std::vector<Request>& request_list) { const static size_t kMaxSliceSize = 16ull << 20 ; auto gds_batch = dynamic_cast <GdsSubBatch*>(batch); size_t num_params = 0 ; size_t first_param_index = gds_batch->io_params.size (); for (auto & request : request_list) num_params += (request.length + kMaxSliceSize - 1 ) / kMaxSliceSize; if (first_param_index + num_params > io_batch_depth_) return Status::TooManyRequests ("Exceed batch capacity" LOC_MARK); for (auto & request : request_list) { GdsFileContext* context = findFileContext (request.target_id); if (!context || !context->ready ()) return Status::InvalidArgument ("Invalid remote segment" LOC_MARK); IOParamRange range; range.base = gds_batch->io_params.size (); for (size_t offset = 0 ; offset < request.length; offset += kMaxSliceSize) { size_t length = std::min (kMaxSliceSize, request.length - offset); const size_t slice_id = gds_batch->io_params.size (); CUfileIOParams_t params; params.mode = CUFILE_BATCH; params.opcode = (request.opcode == Request::READ) ? CUFILE_READ : CUFILE_WRITE; params.cookie = reinterpret_cast <void *>( static_cast <std::uintptr_t >(slice_id + 1 )); params.u.batch.devPtr_base = request.source; params.u.batch.devPtr_offset = offset; params.u.batch.file_offset = request.target_offset + offset; params.u.batch.size = length; params.fh = context->getHandle (); gds_batch->io_params.push_back (params); CUfileIOEvents_t cached_event{}; cached_event.cookie = params.cookie; cached_event.status = CUFILE_PENDING; gds_batch->cached_events.push_back (cached_event); range.count++; } gds_batch->io_param_ranges.push_back (range); } auto result = cuFileBatchIOSubmit (gds_batch->batch_handle->handle, num_params, &gds_batch->io_params[first_param_index], 0 ); return Status::OK (); }
源码:gds_transport.cpp:437-490 。文件上下文的延迟创建见 gds_transport.cpp:404-435 。
把一个 20 MiB 的抽象例子代入上述循环,映射会变成:
逻辑对象
devPtr_offset
file_offset
size
cookie
slice 0
0
target_offset
16 MiB
1
slice 1
16 MiB
target_offset + 16 MiB
4 MiB
2
这个例子只是在代入源码常量,不是性能测试。可以确认的是切片方式;源码没有解释为什么选择 16 MiB,因此不能把它写成 GDS 的通用最优值。
节点五:轮询把 slice 重新聚合成 task TENT 先根据 sub_task_id 找回 GDS task 调用者开始轮询后,TransferEngineImpl::pollTaskStatus 从公共 TaskInfo 取出 transport 和 sub_task_id,再调用相应后端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Status TransferEngineImpl::pollTaskStatus (Batch* batch, size_t task_id, TransferStatus& task_status) { auto & task = batch->task_list[task_id]; auto & transport = transport_list_[task.type]; auto & sub_batch = batch->sub_batch[task.type]; if (!transport || !sub_batch) { return Status::InvalidArgument ("Transport not available" LOC_MARK); } return transport->getTransferStatus (sub_batch, task.sub_task_id, task_status); }
源码:transfer_engine_impl.cpp:2376-2396 。
cuFile event 先写临时数组,再按 cookie 固化 GDS 的一次 status 调用会查询整个 cuFile batch。io_events 只是本轮返回的 scratch buffer,cached_events 才是按 slice 保存的稳定状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Status GdsTransport::updateBatchStatus (GdsSubBatch* batch) { unsigned num_events = static_cast <unsigned >(batch->io_params.size ()); if (num_events == 0 ) return Status::OK (); auto result = cuFileBatchIOGetStatus (batch->batch_handle->handle, 0 , &num_events, batch->io_events.data (), nullptr ); for (size_t index = 0 ; index < num_events; ++index) { const auto & event = batch->io_events[index]; const auto cookie = reinterpret_cast <std::uintptr_t >(event.cookie); if (cookie == 0 || cookie > batch->cached_events.size ()) continue ; auto & cached_event = batch->cached_events[cookie - 1 ]; if (!isTerminalCuFileStatus (cached_event.status) || isTerminalCuFileStatus (event.status)) { cached_event = event; } } return Status::OK (); }
源码:gds_transport.cpp:152-187 。
只有全部 slice 终结,task 才完成 getTransferStatus 先查 IOParamRange。若它还在 PENDING,就更新 batch、聚合 [base, base+count),累加已完成字节;在本文的成功路径上,所有 slice 都是 CUFILE_COMPLETE 后才把 range 置为 COMPLETED:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 auto & range = gds_batch->io_param_ranges[task_id];if (range.status != PENDING) { status = TransferStatus{range.status, range.transferred_bytes}; return Status::OK (); } auto update_status = updateBatchStatus (gds_batch);bool all_terminal = false ;auto task_status = aggregateTransferStatus ( gds_batch->cached_events, range.base, range.count, all_terminal); range.transferred_bytes = std::max (range.transferred_bytes, task_status.transferred_bytes); if (all_terminal) { range.status = range.known_failure != PENDING ? range.known_failure : task_status.s; gds_batch->reusable = allBatchIOsTerminal (gds_batch); } status = TransferStatus{range.status, range.transferred_bytes}; return Status::OK ();
源码:gds_transport.cpp:492-561 ,聚合规则见 gds_transport.cpp:94-150 。仓库测试也明确验证两个完成 slice 的字节数会相加并返回 COMPLETED:gds_transport_status_test.cpp:207-216 。
节点六:终态之后才释放资源 调用者看到终态后执行 freeBatch。默认 queue 关闭且没有额外引用时,TENT 会再次确认整个 Batch 已终结,依次释放各 transport 的 SubBatch,再回收公共 Batch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (!runtime_queue_config_.enabled && batch->runtime_refs == 0 && !batch->free_requested) { TransferStatus overall_status; auto status = getTransferStatus (batch_id, overall_status); if (status.ok () && overall_status.s != PENDING) { for (size_t type = 0 ; type < kSupportedTransportTypes; ++type) { auto & transport = transport_list_[type]; auto & sub_batch = batch->sub_batch[type]; if (transport && sub_batch) transport->freeSubBatch (sub_batch); } Slab<Batch>::Get ().deallocate (batch); return Status::OK (); } }
源码:transfer_engine_impl.cpp:914-950 。
GDS 的正常释放不会立刻销毁昂贵的 cuFile batch handle,而是把它放回 handle_pool_,供下一次 allocateSubBatch 复用:
1 2 3 4 5 6 7 8 9 10 if (reusable) { { std::lock_guard<std::mutex> lock (handle_pool_lock_) ; handle_pool_.push_back (gds_batch->batch_handle); } gds_batch->batch_handle = nullptr ; Slab<GdsSubBatch>::Get ().deallocate (gds_batch); } batch = nullptr ; return Status::OK ();
源码:gds_transport.cpp:364-401 。
这一步补全了资源生命周期:**cuFileBatchIOSetUp 创建的 handle 属于 GDS pool,不属于某一个公共 task;CUfileIOParams_t、event cache 和 IOParamRange 才属于本次 SubBatch。**
把整条路径再串一次 现在沿 20 MiB 读取的抽象例子,把对象与状态放在同一张表里:
时刻
执行函数
关键对象
状态或变化
进入 API
TransferEngine::submitTransfer
Request
READ、本地 buffer、File SegmentID、offset、20 MiB
准备
prepareSubmit
PreparedSubmit::Owner
单请求不合并;selector 解析出 GDS
提交计划
commitPreparedSubmit
Batch::TaskInfo
status=PENDING、type=GDS、sub_task_id=0
分配后端批次
GdsTransport::allocateSubBatch
GdsSubBatch
获得 cuFile batch handle;初始化 range、params、events
翻译请求
submitTransferTasks
IOParamRange{base=0,count=2}
20 MiB 被切成 16 MiB 与 4 MiB 两个 slice
设备提交
cuFileBatchIOSubmit
CUfileIOParams_t[2]
host 同步返回提交结果,I/O 异步推进
状态轮询
cuFileBatchIOGetStatus
CUfileIOEvents_t[2]
event 通过 cookie 写回 cached_events
状态聚合
aggregateTransferStatus
TransferStatus
两个 slice 均完成后返回 COMPLETED 与 20 MiB
释放
freeBatch → freeSubBatch
Batch / SubBatch / handle
公共 Batch 回收,cuFile batch handle 回池
最容易混淆的是三种“数量”:
Batch::max_size 限制公共 task 数;
GdsSubBatch::io_param_ranges.size() 是 GDS 逻辑 task 数;
GdsSubBatch::io_params.size() 是切片后的 cuFile 物理 I/O 数,受 io_batch_depth 限制。
事实核查 按照“外部可验证事实—推导—判断”拆分后,本文的关键结论可以分成四类:
待核查说法
结论
证据与修正
默认请求会直接进入 commitPreparedSubmit
已证实
enable_runtime_queue 默认 false;若用户显式开启 queue,这条主线必须插入 admission 与 dispatch。
File Segment 默认优先 GDS
基本成立,但需要收窄
默认 policy 是 GDS → IOURING,但 GDS 还必须通过构建、配置、实例存在和 capability 检查。
openSegment(file://...) 会打开文件
明显错误
它只登记 SegmentID;stat 在描述解析时发生,open(O_DIRECT) 与 cuFileHandleRegister 在 GDS 首次提交时发生。
registerLocalMemory 会自动对探测到的 CUDA buffer 调用 cuFileBufRegister
证据不支持这一宽泛说法
该实现判断的是 MemoryOptions.location,不是探测后的 BufferDesc.location;MemoryOptions 默认值 和默认 permission 重载 留下的是 "*"。只有显式 CUDA location 才进入 GDS 注册分支 。
未显式 cuFileBufRegister 就不能提交 GDS
明显错误
NVIDIA 明确说明 buffer registration 是可选的;未注册 buffer 可能使用 cuFile 内部预注册 buffer,并多一次 copy。Best Practices
submitTransfer 返回成功表示读取完成
明显错误
它只证明准备和 cuFileBatchIOSubmit 没有同步报错;完成状态来自后续轮询。
一个 TENT task 对应一个 cuFile I/O
基本成立,但需要收窄为一对多
每个 task 对应一个 IOParamRange,再按 16 MiB 上限展开为一个或多个 CUfileIOParams_t。
看到 COMPLETED 时所有 slice 都已终结
已证实
all_terminal 为真后才更新 range 终态;完成字节聚合也有仓库单元测试覆盖。
这次核查里最关键的修正不是函数名,而是不要把“选择了 GDS transport”升级成“已经证明物理 direct path” 。源码能高置信度证明控制流、对象映射和调用参数;它不能替代一台真实 GDS 机器上的 cuFile stats、对齐检查和端到端数据校验。
结语 回头看开头那串箭头,它缺的并不是更多函数名,而是对象身份。
一次成功的 TENT GDS 读取,真正的主线是:Request 先变成调度计划,再变成公共 TaskInfo;GDS 用 sub_task_id 把它映射到 IOParamRange,按 16 MiB 切成 cuFile slice;完成事件依靠 cookie 回填,全部 slice 终结后才重新聚合为公共 TransferStatus。最后,公共 Batch 被释放,昂贵的 cuFile batch handle 则留在池里等待下一次请求。
目前可以相信到这里:固定提交下的成功控制流已经由源码和仓库测试闭环;真实硬件是否走 direct path、性能是否最优,仍需在 GDS 环境中用 cuFile stats 和实际数据校验。
参考文献
Mooncake 89da2c3a
TENT transfer_engine_impl.cpp
TENT transport_selector.cpp
TENT gds_transport.cpp
NVIDIA GDS cuFile API Reference
NVIDIA GDS Best Practices Guide